/*
 *  Copyright (c)  09.04.2007 Philipp Marx.
 *  Permission is granted to copy, distribute and/or modify this document
 *  under the terms of the GNU Free Documentation License, Version 1.2
 *  or any later version published by the Free Software Foundation;
 *  with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
 *  A copy of the license is included in the section entitled "GNU
 *  Free Documentation License".
 */

package de.smigfu.irc.core;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.List;
import java.util.Vector;

import de.smigfu.irc.exception.NoConnectionException;
import de.smigfu.irc.exception.ServerCommunicationException;
import de.smigfu.irc.handler.IRCProtocolMessageHandler;
import de.smigfu.irc.handler.Message;
import de.smigfu.irc.handler.MessageHandler;
import de.smigfu.random.words.RandomWordGenerator;

/**
 * This class represents one "connection" to an IRC-server. By calling the static method
 * {@link #openConnection(String, int, String, String)} the "connection" is established and registerd. 
 * A separte "listening"-thread ist started. 
 * A arbitary number of <code>MessageHandler</code> can be added. Each is informed for every 
 * message received by this "connection".
 * There are <code>MessageHandler</code>s allready registered to maintain the "connection".
 * Serveral parameters a read from a config file which must be provided. For an example look at
 * \/conf/config.properties.
 * You can provide a perform file. All command in there will be executed line by line. For an example 
 * look at \/conf/perform.txt.
 * 
 * @version $Revision$ $Date$
 *
 * @author Philipp Marx, last modified by $Author$
 */
public class IRCServerConnection {

   /**
    * Opens and configures a new <code>IRCServerConnection</code>. The "connection" is established,
    * registered and a <code>IRCCommandMessageHandler</code> is registered to maintain the
    * "connection".
    * There are specific <code>Exception</code>s thrown in case of an error by trying to connect.
    * This method only tries once to connect. 
    * 
    * @param host The host to connect to.
    * @param port The port.
    * @param nick The nick or <code>null</code> for a generated nick.
    * @param realname The realname or <code>null</code> for a generated name.
    * @return The established "connection".
    * @throws ServerCommunicationException If an <code>Exception</code> is thrown while trying to 
    *                                      connect.
    */
   public static IRCServerConnection openConnection(final String host,
                                                    final int port,
                                                    final String nick,
                                                    final String realname)
         throws ServerCommunicationException {

      final IRCServerConnection connection = new IRCServerConnection();

      // Establish the connection.
      connection.connect(host, port);
      connection.register(nick, realname);
      connection.addMessageHandler(new IRCProtocolMessageHandler());

      return connection;
   }

   /** Flag, indicating if the "connection" is established. */
   volatile boolean connected = false;

   /** List containing alle <code>MessageHandler</code>s. */
   private List<MessageHandler> messageHandlers = null;

   /** The mutex for synchronization */
   private Object mutex = null;

   /** The <code>BufferedReader</code> for reading messages from the server. */
   private BufferedReader reader = null;

   /** The <code>BufferedWriter</code> for writing messages to the server. */
   private BufferedWriter writer = null;

   /**
    * Constructs a <code>IRCServerConnection</code>. An <code>IRCServerConnection</code> can only
    * be retrieved by the static methods provided.
    *
    */
   private IRCServerConnection() {
      this.mutex = new Object();

      this.messageHandlers = new Vector<MessageHandler>();
   }

   /**
    * Adds a new <code>MessageHandler</code>.
    * 
    * @param messageHandler The <code>MessageHandler</code> to add.
    * @throws NullPointerException If "messageHandler" is <code>null</code>.
    */
   public final void addMessageHandler(final MessageHandler messageHandler) {
      if (messageHandler == null) {
         throw new NullPointerException();
      }

      this.messageHandlers.add(messageHandler);
   }

   /**
    * Opens an TCP/IP-<code>Socket</code> to the host and port specified in the properties. After 
    * successfully connected a "listening"-thread is started to receive incoming messages.
    * If this is called while allready connected, the current connection is "disconnected". 
    * <code>MessageHandler</code>s allready connected will not be removed.
    * 
    * @param host The host to connect to.
    * @param port The port.
    * @throws ServerCommunicationException If an <code>Exception</code> is thrown while trying to 
    *                                      connect.
    */
   public final void connect(final String host, final int port) throws ServerCommunicationException {

      if (host == null) {
         throw new NullPointerException();
      }
      if ((port < 0) || (port > 65536)) {
         throw new IllegalArgumentException("Port range not valid. Must be be between 0 and 65536!");
      }

      // If allready connected => disconnect.
      if (this.connected) {
         this.disconnect();
      }

      // Set up the socket and streams.
      try {
         final Socket socket = new Socket(host, port);

         this.reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
         this.writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

         this.connected = true;

         // Start listening for messages.
         this.startListening();
      } catch (final IOException e) {
         e.printStackTrace();
         throw new ServerCommunicationException(e, ("Connection to " + host + ":" + port).intern());
      }
   }

   /**
    * Disconnects this "connection" by sending the CMD_QUIT-Command to the server. Closes all streams
    * and stops the "listening"-thread. 
    * 
    * @throws ServerCommunicationException If an <code>Exception</code> is thrown while disconnecting.
    */
   public final void disconnect() throws ServerCommunicationException {

      // Send CMD_QUIT to the server.
      this.writeCommand(IRCProtocolConstants.CMD_QUIT);

      try {
         // Close the streams.
         this.getWriter().close();
         this.getReader().close();
      } catch (final IOException e) {
         e.printStackTrace();
      }

      // Stop listening.
      this.stopListening();
   }

   /**
    * Notifies all registered <code>MessageHandler</code> about a received message.
    * 
    * @param message The received message.
    */
   public final void notifyMessageReceived(final String message) {
      synchronized (this.mutex) {
         for (final MessageHandler handler : this.messageHandlers) {
            handler.handleMessageReceived(this, new Message(message));
         }
      }
   }

   /**
    * Notifies all registered <code>MessageHandler</code> about a sent message.
    * 
    * @param message The sent message.
    */
   public final void notifyMessageSent(final String message) {
      synchronized (this.mutex) {
         for (final MessageHandler handler : this.messageHandlers) {
            handler.handleMessageReceived(this, new Message(message));
         }
      }
   }

   /**
    * Registers this "client-connection" at the IRC-server. The connection is registered by sending
    * ths CMD_PASS, CMD_NICK and CMD_USER command. The parameters for these commands are retrieved from the 
    * <code>UserPreferences</code>.
    * 
    * @param nick The nick or <code>null</code> for a generated nick.
    * @param realname The realname or <code>null</code> for a generated name.
    * @throws ServerCommunicationException If an <code>Exception</code> is thrown while registering.
    */
   public void register(String nick, String realname) throws ServerCommunicationException {

      if (nick == null) {
         nick = RandomWordGenerator.getInstance().getNextWord();
      }
      if (realname == null) {
         realname = RandomWordGenerator.getInstance().getNextWord();
      }

      // Send CMD_PASS.
      StringBuilder buffer = new StringBuilder();
      buffer.append(IRCProtocolConstants.CMD_PASS);
      buffer.append(" ");
      buffer.append("dummypass123");
      this.writeCommand(buffer.toString());

      // Send CMD_NICK.
      buffer = new StringBuilder();
      buffer.append(IRCProtocolConstants.CMD_NICK);
      buffer.append(" ");
      buffer.append(nick);
      this.writeCommand(buffer.toString());

      // Send CMD_USER.
      buffer = new StringBuilder();
      buffer.append(IRCProtocolConstants.CMD_USER);
      buffer.append(" ");
      buffer.append(nick);
      buffer.append(" 0 0 :");
      buffer.append(realname);
      this.writeCommand(buffer.toString());
   }

   /**
    * Removes the <code>MessageHandler</code>.
    * 
    * @param messageHandler The <code>MessageHandler</code> to remove.
    * @throws NullPointerException If "messageHandler" is <code>null</code>.
    */
   public final void removeMessageHandler(final MessageHandler messageHandler) {
      if (messageHandler == null) {
         throw new NullPointerException();
      }

      this.messageHandlers.remove(messageHandler);
   }

   /**
    * Writes a command to the connected server. Adds a VAR_LF at the end of the command.
    * 
    * @param command The command.
    * @throws ServerCommunicationException If an <code>Exception</code> is thrown while writing the
    *                                      command.
    * @throws NoConnectionException If the "connection" is not established.                                            
    */
   public final void writeCommand(final String command) throws ServerCommunicationException {
      if (!this.connected) {
         throw new NoConnectionException();
      }

      try {
         final StringBuilder buffer = new StringBuilder(command.length() + 1);
         buffer.append(command);
         buffer.append(IRCProtocolConstants.VAR_LF);
         this.getWriter().write(buffer.toString());
         this.getWriter().flush();
         System.out.println("SEND: " + command);
      } catch (final IOException e) {
         System.out.println("FAILED SENDING: " + command);
         throw new ServerCommunicationException(e, ("Writing message: " + command).intern());
      }
   }

   /**
    * Writes a "priv"-message to the connected server and the specified destination. A destination
    * is either another user or a channel (leading with an '#'). 
    * 
    * @param message The message.
    * @param destination The destination.
    * @throws ServerCommunicationException If an <code>Exception</code> is thrown while writing the
    *                                      message.
    * @throws NoConnectionException If the "connection" is not established.                                             
    */
   public final void writePrivMessage(final String message, final String destination)
         throws ServerCommunicationException {
      if (!this.connected) {
         throw new NoConnectionException();
      }

      final StringBuilder buffer = new StringBuilder();
      buffer.append(IRCProtocolConstants.CMD_PRIVMSG);
      buffer.append(" ");
      buffer.append(destination);
      buffer.append(" :");
      buffer.append(message);

      this.writeCommand(buffer.toString());
   }

   /**
    * Reads a line from the established "connection". This method will block until a message (a line)
    * is received.
    * 
    * @return The message.
    * @throws ServerCommunicationException If an <code>Exception</code> is thrown while reading the
    *                                      message.
    * @throws NoConnectionException If the "connection" is not established.                                   
    */
   String readMessage() throws ServerCommunicationException {
      if (!this.connected) {
         throw new NoConnectionException();
      }

      String message = null;
      try {
         message = this.getReader().readLine();
         System.out.println("READ: " + message);
      } catch (final IOException e) {
         System.out.println("FAILED READNIG: " + message);
         throw new ServerCommunicationException(e, ("Reading message: " + message).intern());
      }

      return message;
   }

   /**
    * Returns the <code>Reader</code>.
    *  
    * @return The Reader.
    * @throws NoConnectionException If the "connection" is not established.
    */
   private BufferedReader getReader() {
      if (!this.connected) {
         throw new NoConnectionException();
      }

      return this.reader;
   }

   /**
    * Returns the <code>Writer</code>.
    * 
    * @return The Writer.
    * @throws NoConnectionException If the "connection" is not established.
    */
   private BufferedWriter getWriter() {
      if (!this.connected) {
         throw new NoConnectionException();
      }

      return this.writer;
   }

   /**
    * Start the "listening"-thread.
    *
    */
   private void startListening() {
      synchronized (this.mutex) {
         final Thread t = new Thread() {

            /* (non-Javadoc)
             * @see java.lang.Thread#run()
             */
            @Override
            public void run() {

               while (IRCServerConnection.this.connected) {
                  try {

                     // Read the message and handle it.
                     IRCServerConnection.this.notifyMessageReceived(IRCServerConnection.this.readMessage());
                  } catch (ServerCommunicationException e) {
                     e.printStackTrace();
                     //TODO das muss nach aussen gegeben werden
                  }
               }
            }
         };

         t.start();
      }
   }

   /**
    * Stops listening to incoming messages. If currently a message is handled, the handling will be
    * interrupted.
    *
    */
   private void stopListening() {
      synchronized (this.mutex) {
         this.connected = false;
      }
   }
}
